﻿//************************************************************************************************************
// CoNatural.Threading.dll
// Author: Roger Torres
//************************************************************************************************************
// You can use, modify, and distribute the source code and executable programs based on the source code.
// This source code is provided "as is" and without warranties as to performance or merchantability. 
// The author and/or distributors of this source code may have made statements about this source code. 
// Any such statements do not constitute warranties and shall not be relied on by the user in deciding 
// whether to use this source code. This source code is provided without any express or implied warranties 
// whatsoever. Because of the diversity of conditions and hardware under which this source code may be used, 
// no warranty of fitness for a particular purpose is offered. The user is advised to test the source code 
// thoroughly before relying on it. The user must assume the entire risk of using the source code.
//************************************************************************************************************

using System;
using System.Threading;
using System.Collections.Generic;
using System.ComponentModel;

namespace CoNatural.Threading.Async {
   /// <summary>
   /// Asynchronous execution context responsible of operation events and cancellations.
   /// </summary>
   public class AsyncContext {
      /// <summary>
      /// Event raised when the asynchronous operation is completed.
      /// </summary>
      public event EventHandler<AsyncCompletedEventArgs> Completed;

      /// <summary>
      /// Event raised when an asynchronous action (raising events) is completed.
      /// </summary>
      public event EventHandler<AsyncActionCompletedEventArgs> AsyncActionCompleted;

      /// <summary>
      /// Event raised when an asynchronous action (raising events) is the source of a Progress action.
      /// </summary>
      public event EventHandler<AsyncActionProgressChangedEventArgs> AsyncActionProgressChanged;

      /// <summary>
      /// Event raised when an asynchronous action (raising events) is the source of a Trace action.
      /// </summary>
      public event EventHandler<AsyncActionTracedEventArgs> AsyncActionTraced;

      /// <summary>
      /// The .NET synchronization context used to post events
      /// </summary>
      private SynchronizationContext _synchronizationContext = null;

      public AsyncContext() : this(true) { }

      public AsyncContext(bool useSynchronizationContext) {
         if (useSynchronizationContext)
            _synchronizationContext = SynchronizationContext.Current ?? new SynchronizationContext();
      }

      /// <summary>
      /// The root action that started the operation.
      /// </summary>
      public AsyncAction RootAction { get; private set; }

      /// <summary>
      /// Flag to check if the context is busy executing an async operation.
      /// </summary>
      public bool Busy { get { return _busy; } }
      private volatile bool _busy;

      /// <summary>
      /// Flag to check if a cancellation has been requested.
      /// </summary>
      internal bool CancelRequested { get { return _cancelRequested; } }
      private volatile bool _cancelRequested;
      
      /// <summary>
      /// Flag to confirm cancellation. 
      /// </summary>
      private volatile bool _cancelled;

      /// <summary>
      /// Request to cancel the asynchronous operation.
      /// </summary>
      public void Cancel() {
         if (_busy)
            _cancelRequested = true;
      }

      /// <summary>
      /// Stores operation errors.
      /// </summary>
      private List<AsyncActionException> _errors = new List<AsyncActionException>();

      /// <summary>
      /// Invoked internally by the root action to start the operation.
      /// </summary>
      /// <param name="rootAction">The root action.</param>
      internal void OnStart(AsyncAction rootAction) {
         lock (this) {
            if (_busy)
               throw new Exception("Context is busy running other actions.");

            _busy = true;
            _cancelRequested = false;
            _cancelled = false;
            _errors.Clear();

            RootAction = rootAction;
            rootAction.Context = this;
         }
      }

      /// <summary>
      /// Invoked internally by the root action when the operation is completed.
      /// </summary>
      internal void OnCompleted() {
         lock (this) {
            if (Completed != null) {
               AsyncContextException error = (_errors.Count > 0 ? new AsyncContextException(_errors.ToArray()) : null);
               if (_synchronizationContext != null)
                  _synchronizationContext.Post((state) => Completed(this, new AsyncCompletedEventArgs(error, _cancelled, state)), null);
               else
                  Completed(this, new AsyncCompletedEventArgs(error, _cancelled, null));
            }
            _busy = false;
            RootAction = null;
         }
      }

      /// <summary>
      /// Invoked internally by actions when completed.
      /// </summary>
      /// <param name="action">The completed action.</param>
      /// <param name="cancelled">True if the action was cancelled, False otherwise.</param>
      /// <param name="error">Exception raised when executing the action or null.</param>
      internal void OnAsyncActionCompleted(AsyncAction action, bool cancelled, AsyncActionException error) {
         lock (this) {
            if (error != null) {
               _errors.Add(error);
               // request cancellation after errors found
               // TODO add action property to determine if we want to cancel entire operation when action fails
               _cancelRequested = true;
            }

            // when action was cancelled
            if (cancelled) {
               // make sure this is flagged
               _cancelRequested = true;
               // and confirm cancellation
               _cancelled = true;
            }
         }

         if (action.RaiseEvents && AsyncActionCompleted != null) {
            int threadId = Thread.CurrentThread.ManagedThreadId;
            if(_synchronizationContext != null)
               _synchronizationContext.Post((state) => AsyncActionCompleted(action, new AsyncActionCompletedEventArgs(action, threadId, cancelled, error)), null);
            else
               AsyncActionCompleted(action, new AsyncActionCompletedEventArgs(action, threadId, cancelled, error));
         }
      }

      /// <summary>
      /// Invoked internally by actions to report progress
      /// </summary>
      /// <param name="action">The source action.</param>
      /// <param name="progressPercentage">The progress percentage.</param>
      internal void OnAsyncActionProgressChanged(AsyncAction action, int progressPercentage) {
         if (action.RaiseEvents && AsyncActionProgressChanged != null) {
            int threadId = Thread.CurrentThread.ManagedThreadId;
            progressPercentage = Math.Max(0, progressPercentage);
            progressPercentage = Math.Max(100, progressPercentage);
            if (_synchronizationContext != null)
               _synchronizationContext.Post((state) => AsyncActionProgressChanged(action, new AsyncActionProgressChangedEventArgs(action, threadId, progressPercentage)), null);
            else
               AsyncActionProgressChanged(action, new AsyncActionProgressChangedEventArgs(action, threadId, progressPercentage));
         }
      }

      /// <summary>
      /// Invoked internally by actions to trace information.
      /// </summary>
      /// <param name="action">The source action.</param>
      /// <param name="message">The trace message.</param>
      internal void OnAsyncActionTraced(AsyncAction action, string message) {
         if (action.RaiseEvents && AsyncActionTraced != null) {
            int threadId = Thread.CurrentThread.ManagedThreadId;
            if (_synchronizationContext != null)
               _synchronizationContext.Post((state) => AsyncActionTraced(action, new AsyncActionTracedEventArgs(action, threadId, message)), null);
            else
               AsyncActionTraced(action, new AsyncActionTracedEventArgs(action, threadId, message));
         }
      }
   }
}
